/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.ziav.rpc.utils;

import static cn.ziav.rpc.utils.CollectionUtils.ofSet;
import static cn.ziav.rpc.utils.Streams.filterAll;
import static java.util.Arrays.asList;
import static java.util.Collections.emptySet;
import static java.util.Collections.unmodifiableSet;
import static java.util.stream.Collectors.toList;
import static org.apache.commons.lang3.ArrayUtils.isNotEmpty;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;

public class ClassUtils {
  /** Suffix for array class names: "[]" */
  public static final String ARRAY_SUFFIX = "[]";
  /** Prefix for internal array class names: "[L" */
  private static final String INTERNAL_ARRAY_PREFIX = "[L";
  /**
   * Map with primitive type name as key and corresponding primitive type as value, for example:
   * "int" -> "int.class".
   */
  private static final Map<String, Class<?>> PRIMITIVE_TYPE_NAME_MAP =
      new HashMap<String, Class<?>>(32);
  /**
   * Map with primitive wrapper type as key and corresponding primitive type as value, for example:
   * Integer.class -> int.class.
   */
  private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_TYPE_MAP =
      new HashMap<Class<?>, Class<?>>(16);

  /**
   * Simple Types including:
   *
   * <ul>
   *   <li>{@link Void}
   *   <li>{@link Boolean}
   *   <li>{@link Character}
   *   <li>{@link Byte}
   *   <li>{@link Integer}
   *   <li>{@link Float}
   *   <li>{@link Double}
   *   <li>{@link String}
   *   <li>{@link BigDecimal}
   *   <li>{@link BigInteger}
   *   <li>{@link Date}
   *   <li>{@link Object}
   * </ul>
   *
   * @see javax.management.openmbean.SimpleType
   * @since 2.7.6
   */
  public static final Set<Class<?>> SIMPLE_TYPES =
      ofSet(
          Void.class,
          Boolean.class,
          Character.class,
          Byte.class,
          Short.class,
          Integer.class,
          Long.class,
          Float.class,
          Double.class,
          String.class,
          BigDecimal.class,
          BigInteger.class,
          Date.class,
          Object.class);

  private static final char PACKAGE_SEPARATOR_CHAR = '.';

  static {
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Boolean.class, boolean.class);
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Byte.class, byte.class);
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Character.class, char.class);
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Double.class, double.class);
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Float.class, float.class);
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Integer.class, int.class);
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Long.class, long.class);
    PRIMITIVE_WRAPPER_TYPE_MAP.put(Short.class, short.class);

    Set<Class<?>> primitiveTypeNames = new HashSet<>(32);
    primitiveTypeNames.addAll(PRIMITIVE_WRAPPER_TYPE_MAP.values());
    primitiveTypeNames.addAll(
        Arrays.asList(
            boolean[].class,
            byte[].class,
            char[].class,
            double[].class,
            float[].class,
            int[].class,
            long[].class,
            short[].class));
    for (Class<?> primitiveTypeName : primitiveTypeNames) {
      PRIMITIVE_TYPE_NAME_MAP.put(primitiveTypeName.getName(), primitiveTypeName);
    }
  }

  public static Class<?> forNameWithThreadContextClassLoader(String name)
      throws ClassNotFoundException {
    return forName(name, Thread.currentThread().getContextClassLoader());
  }

  public static Class<?> forNameWithCallerClassLoader(String name, Class<?> caller)
      throws ClassNotFoundException {
    return forName(name, caller.getClassLoader());
  }

  public static ClassLoader getCallerClassLoader(Class<?> caller) {
    return caller.getClassLoader();
  }

  /**
   * get class loader
   *
   * @param clazz
   * @return class loader
   */
  public static ClassLoader getClassLoader(Class<?> clazz) {
    ClassLoader cl = null;
    try {
      cl = Thread.currentThread().getContextClassLoader();
    } catch (Throwable ex) {
      // Cannot access thread context ClassLoader - falling back to system class loader...
    }
    if (cl == null) {
      // No thread context class loader -> use class loader of this class.
      cl = clazz.getClassLoader();
      if (cl == null) {
        // getClassLoader() returning null indicates the bootstrap ClassLoader
        try {
          cl = ClassLoader.getSystemClassLoader();
        } catch (Throwable ex) {
          // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
        }
      }
    }

    return cl;
  }

  /**
   * Return the default ClassLoader to use: typically the thread context ClassLoader, if available;
   * the ClassLoader that loaded the ClassUtils class will be used as fallback.
   *
   * <p>Call this method if you intend to use the thread context ClassLoader in a scenario where you
   * absolutely need a non-null ClassLoader reference: for example, for class path resource loading
   * (but not necessarily for <code>Class.forName</code>, which accepts a <code>null</code>
   * ClassLoader reference as well).
   *
   * @return the default ClassLoader (never <code>null</code>)
   * @see Thread#getContextClassLoader()
   */
  public static ClassLoader getClassLoader() {
    return getClassLoader(ClassUtils.class);
  }

  /** Same as <code>Class.forName()</code>, except that it works for primitive types. */
  public static Class<?> forName(String name) throws ClassNotFoundException {
    return forName(name, getClassLoader());
  }

  /**
   * Replacement for <code>Class.forName()</code> that also returns Class instances for primitives
   * (like "int") and array class names (like "String[]").
   *
   * @param name the name of the Class
   * @param classLoader the class loader to use (may be <code>null</code>, which indicates the
   *     default class loader)
   * @return Class instance for the supplied name
   * @throws ClassNotFoundException if the class was not found
   * @throws LinkageError if the class file could not be loaded
   * @see Class#forName(String, boolean, ClassLoader)
   */
  public static Class<?> forName(String name, ClassLoader classLoader)
      throws ClassNotFoundException, LinkageError {

    Class<?> clazz = resolvePrimitiveClassName(name);
    if (clazz != null) {
      return clazz;
    }

    // "java.lang.String[]" style arrays
    if (name.endsWith(ARRAY_SUFFIX)) {
      String elementClassName = name.substring(0, name.length() - ARRAY_SUFFIX.length());
      Class<?> elementClass = forName(elementClassName, classLoader);
      return Array.newInstance(elementClass, 0).getClass();
    }

    // "[Ljava.lang.String;" style arrays
    int internalArrayMarker = name.indexOf(INTERNAL_ARRAY_PREFIX);
    if (internalArrayMarker != -1 && name.endsWith(";")) {
      String elementClassName = null;
      if (internalArrayMarker == 0) {
        elementClassName = name.substring(INTERNAL_ARRAY_PREFIX.length(), name.length() - 1);
      } else if (name.startsWith("[")) {
        elementClassName = name.substring(1);
      }
      Class<?> elementClass = forName(elementClassName, classLoader);
      return Array.newInstance(elementClass, 0).getClass();
    }

    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
      classLoaderToUse = getClassLoader();
    }
    return classLoaderToUse.loadClass(name);
  }

  /**
   * Resolve the given class name as primitive class, if appropriate, according to the JVM's naming
   * rules for primitive classes.
   *
   * <p>Also supports the JVM's internal class names for primitive arrays. Does <i>not</i> support
   * the "[]" suffix notation for primitive arrays; this is only supported by {@link #forName}.
   *
   * @param name the name of the potentially primitive class
   * @return the primitive class, or <code>null</code> if the name does not denote a primitive class
   *     or primitive array class
   */
  public static Class<?> resolvePrimitiveClassName(String name) {
    Class<?> result = null;
    // Most class names will be quite long, considering that they
    // SHOULD sit in a package, so a length check is worthwhile.
    if (name != null && name.length() <= 8) {
      // Could be a primitive - likely.
      result = (Class<?>) PRIMITIVE_TYPE_NAME_MAP.get(name);
    }
    return result;
  }

  public static String toShortString(Object obj) {
    if (obj == null) {
      return "null";
    }
    return obj.getClass().getSimpleName() + "@" + System.identityHashCode(obj);
  }

  public static String simpleClassName(Class<?> clazz) {
    if (clazz == null) {
      throw new NullPointerException("clazz");
    }
    String className = clazz.getName();
    final int lastDotIdx = className.lastIndexOf(PACKAGE_SEPARATOR_CHAR);
    if (lastDotIdx > -1) {
      return className.substring(lastDotIdx + 1);
    }
    return className;
  }

  /**
   * The specified type is primitive type or simple type
   *
   * @param type the type to test
   * @return
   * @deprecated as 2.7.6, use {@link Class#isPrimitive()} plus {@link #isSimpleType(Class)} instead
   */
  public static boolean isPrimitive(Class<?> type) {
    return type != null && (type.isPrimitive() || isSimpleType(type));
  }

  /**
   * The specified type is simple type or not
   *
   * @param type the type to test
   * @return if <code>type</code> is one element of {@link #SIMPLE_TYPES}, return <code>true</code>,
   *     or <code>false</code>
   * @see #SIMPLE_TYPES
   * @since 2.7.6
   */
  public static boolean isSimpleType(Class<?> type) {
    return SIMPLE_TYPES.contains(type);
  }

  public static Object convertPrimitive(Class<?> type, String value) {
    if (value == null) {
      return null;
    } else if (type == char.class || type == Character.class) {
      return value.length() > 0 ? value.charAt(0) : '\0';
    } else if (type == boolean.class || type == Boolean.class) {
      return Boolean.valueOf(value);
    }
    try {
      if (type == byte.class || type == Byte.class) {
        return Byte.valueOf(value);
      } else if (type == short.class || type == Short.class) {
        return Short.valueOf(value);
      } else if (type == int.class || type == Integer.class) {
        return Integer.valueOf(value);
      } else if (type == long.class || type == Long.class) {
        return Long.valueOf(value);
      } else if (type == float.class || type == Float.class) {
        return Float.valueOf(value);
      } else if (type == double.class || type == Double.class) {
        return Double.valueOf(value);
      }
    } catch (NumberFormatException e) {
      return null;
    }
    return value;
  }

  /**
   * We only check boolean value at this moment.
   *
   * @param type
   * @param value
   * @return
   */
  public static boolean isTypeMatch(Class<?> type, String value) {
    if ((type == boolean.class || type == Boolean.class)
        && !("true".equals(value) || "false".equals(value))) {
      return false;
    }
    return true;
  }

  /**
   * Get all super classes from the specified type
   *
   * @param type the specified type
   * @param classFilters the filters for classes
   * @return non-null read-only {@link Set}
   * @since 2.7.6
   */
  public static Set<Class<?>> getAllSuperClasses(
      Class<?> type, Predicate<Class<?>>... classFilters) {

    Set<Class<?>> allSuperClasses = new LinkedHashSet<>();

    Class<?> superClass = type.getSuperclass();

    if (superClass != null) {
      // add current super class
      allSuperClasses.add(superClass);
      // add ancestor classes
      allSuperClasses.addAll(getAllSuperClasses(superClass));
    }

    return unmodifiableSet(filterAll(allSuperClasses, classFilters));
  }

  /**
   * Get all interfaces from the specified type
   *
   * @param type the specified type
   * @param interfaceFilters the filters for interfaces
   * @return non-null read-only {@link Set}
   * @since 2.7.6
   */
  public static Set<Class<?>> getAllInterfaces(
      Class<?> type, Predicate<Class<?>>... interfaceFilters) {

    if (type == null || type.isPrimitive()) {
      return emptySet();
    }

    Set<Class<?>> allInterfaces = new LinkedHashSet<>();

    Class<?>[] interfaces = type.getInterfaces();

    if (isNotEmpty(interfaces)) {
      // add current interfaces
      allInterfaces.addAll(asList(interfaces));
    }

    // add all super interfaces
    getAllSuperClasses(type)
        .forEach(superType -> allInterfaces.addAll(getAllInterfaces(superType)));

    // add all super interfaces from all interfaces
    allInterfaces.stream()
        .map(ClassUtils::getAllInterfaces)
        .flatMap(Collection::stream)
        .collect(toList())
        .forEach(allInterfaces::add);

    return filterAll(allInterfaces, interfaceFilters);
  }

  /**
   * Get all inherited types from the specified type
   *
   * @param type the specified type
   * @param typeFilters the filters for types
   * @return non-null read-only {@link Set}
   * @since 2.7.6
   */
  public static Set<Class<?>> getAllInheritedTypes(
      Class<?> type, Predicate<Class<?>>... typeFilters) {
    // Add all super classes
    Set<Class<?>> types = new LinkedHashSet<>(getAllSuperClasses(type, typeFilters));
    // Add all interface classes
    types.addAll(getAllInterfaces(type, typeFilters));
    return unmodifiableSet(types);
  }

  /**
   * the semantics is same as {@link Class#isAssignableFrom(Class)}
   *
   * @param superType the super type
   * @param targetType the target type
   * @return see {@link Class#isAssignableFrom(Class)}
   * @since 2.7.6
   */
  public static boolean isAssignableFrom(Class<?> superType, Class<?> targetType) {
    // any argument is null
    if (superType == null || targetType == null) {
      return false;
    }
    // equals
    if (Objects.equals(superType, targetType)) {
      return true;
    }
    // isAssignableFrom
    return superType.isAssignableFrom(targetType);
  }

  /**
   * Test the specified class name is present in the {@link ClassLoader}
   *
   * @param className the name of {@link Class}
   * @param classLoader {@link ClassLoader}
   * @return If found, return <code>true</code>
   * @since 2.7.6
   */
  public static boolean isPresent(String className, ClassLoader classLoader) {
    try {
      forName(className, classLoader);
    } catch (Throwable ignored) { // Ignored
      return false;
    }
    return true;
  }

  /**
   * Resolve the {@link Class} by the specified name and {@link ClassLoader}
   *
   * @param className the name of {@link Class}
   * @param classLoader {@link ClassLoader}
   * @return If can't be resolved , return <code>null</code>
   * @since 2.7.6
   */
  public static Class<?> resolveClass(String className, ClassLoader classLoader) {
    Class<?> targetClass = null;
    try {
      targetClass = forName(className, classLoader);
    } catch (Throwable ignored) { // Ignored
    }
    return targetClass;
  }

  /**
   * Is generic class or not?
   *
   * @param type the target type
   * @return if the target type is not null or <code>void</code> or Void.class, return <code>true
   *     </code>, or false
   * @since 2.7.6
   */
  public static boolean isGenericClass(Class<?> type) {
    return type != null && !void.class.equals(type) && !Void.class.equals(type);
  }
}
